<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文字转图片工具</title>
    <link rel="icon" href="./favicon.ico" />
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial,
          sans-serif;
      }

      :root {
        --primary-bg: #e6e7ee;
        --secondary-bg: #ffffff;
        --shadow-light: #ffffff;
        --shadow-dark: #d1d9e6;
        --accent-color: #667eea;
        --accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        --success-color: #4ecdc4;
        --danger-color: #ff6b6b;
        --warning-color: #feca57;
        --text-primary: #2d3748;
        --text-secondary: #718096;
        --glass-bg: rgba(255, 255, 255, 0.25);
        --glass-border: rgba(255, 255, 255, 0.18);
        --border-radius: 20px;
        --shadow-small: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        --shadow-medium: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        --shadow-large: 16px 16px 32px var(--shadow-dark),
          -16px -16px 32px var(--shadow-light);
      }

      body {
        background: var(--primary-bg);
        color: var(--text-primary);
        min-height: 100vh;
        padding: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
        transition: all 0.3s ease;
        animation: pageLoad 0.8s ease;
      }

      @keyframes pageLoad {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .container {
        width: 100%;
        max-width: 1200px;
        background: var(--primary-bg);
        border-radius: 30px;
        padding: 40px;
        box-shadow: 20px 20px 60px var(--shadow-dark),
          -20px -20px 60px var(--shadow-light);
        margin-top: 20px;
        transition: all 0.3s ease;
        animation: containerFloat 1s ease 0.2s both;
      }

      @keyframes containerFloat {
        from {
          opacity: 0;
          transform: translateY(30px) scale(0.95);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      header {
        text-align: center;
        margin-bottom: 40px;
        padding: 30px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: headerSlide 0.8s ease 0.4s both;
      }

      @keyframes headerSlide {
        from {
          opacity: 0;
          transform: translateY(-30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      h1 {
        font-size: 3rem;
        margin-bottom: 15px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        font-weight: 700;
        animation: titleGlow 3s ease-in-out infinite alternate;
      }

      @keyframes titleGlow {
        from {
          filter: brightness(1);
        }
        to {
          filter: brightness(1.2);
        }
      }

      .subtitle {
        font-size: 1.2rem;
        color: var(--text-secondary);
        margin-bottom: 30px;
        font-weight: 500;
      }

      .panels {
        display: flex;
        gap: 30px;
        margin-bottom: 40px;
      }

      .panel {
        flex: 1;
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        transition: all 0.3s ease;
        position: relative;
        overflow: hidden;
      }

      .panel-title {
        font-size: 1.4rem;
        margin-bottom: 20px;
        color: var(--accent-color);
        display: flex;
        align-items: center;
        font-weight: 600;
      }
      .panel-title::before {
        content: attr(data-icon);
        margin-right: 10px;
        font-size: 1.6rem;
      }

      .file-upload {
        border: 3px dashed var(--accent-color);
        border-radius: 25px;
        padding: 24px;
        background: var(--primary-bg);
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
      }

      .options-form {
        display: flex;
        gap: 20px;
        margin-top: 10px;
        flex-wrap: wrap;
        align-items: flex-end;
      }
      .form-group {
        flex: 1;
        min-width: 180px;
      }
      label {
        display: block;
        margin-bottom: 8px;
        color: var(--text-secondary);
        font-weight: 500;
        font-size: 0.95rem;
      }
      input,
      select {
        width: 100%;
        padding: 12px 16px;
        background: var(--primary-bg);
        border: none;
        border-radius: 15px;
        color: var(--text-primary);
        font-size: 1rem;
        font-weight: 500;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }

      .actions {
        display: flex;
        justify-content: center;
        gap: 20px;
        margin-top: 24px;
      }
      button {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 12px 24px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        font-size: 1rem;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }
      .btn-primary {
        background: var(--accent-gradient);
        color: #fff;
      }
      .btn-success {
        background: linear-gradient(
          135deg,
          var(--success-color) 0%,
          #44a08d 100%
        );
        color: #fff;
      }
      .btn-danger {
        background: linear-gradient(
          135deg,
          var(--danger-color) 0%,
          #e84118 100%
        );
        color: #fff;
      }

      .progress-container {
        margin: 20px 0;
      }
      .progress-bar {
        height: 12px;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }
      .progress-fill {
        height: 100%;
        background: var(--accent-gradient);
        width: 0%;
        transition: width 0.3s ease;
        border-radius: 20px;
      }
      .progress-text {
        text-align: center;
        font-size: 0.95rem;
        color: var(--text-secondary);
        font-weight: 500;
        margin-top: 8px;
      }

      .status-message {
        min-height: 24px;
        margin: 16px 0;
        color: var(--accent-color);
        font-weight: 600;
        font-size: 1rem;
      }
      .error-message {
        color: var(--danger-color);
        min-height: 24px;
        font-weight: 600;
        font-size: 1rem;
      }

      /* 粘贴区域：确保保留原格式 */
      .paste-wrapper {
        background: var(--secondary-bg);
        border-radius: 16px;
        padding: 16px;
        box-shadow: var(--shadow-small);
        max-height: 300px;
        overflow: auto;
      }
      #pasteArea {
        min-height: 220px;
        width: 100%;
        border-radius: 12px;
        background: #fff;
        padding: 12px;
        overflow: auto;
        outline: none;
        /* 尽可能隔离页面全局样式对内部内容的影响，以保留格式 */
        all: initial;
        display: block;
        /* 恢复必要属性 */
        font: revert;
        color: revert;
        text-align: left;
        white-space: normal;
      }
      #pasteArea * {
        all: revert;
      }
      #pastePlaceholder {
        color: #999;
        font-size: 0.95rem;
      }

      /* 捕获区域（用于导出图片） */
      .capture-outer {
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 16px;
        background: var(--secondary-bg);
        border-radius: 16px;
        box-shadow: var(--shadow-medium);
      }
      #captureArea {
        background: transparent;
        border-radius: 8px;
        overflow: visible;
      }

      .download-section {
        display: flex;
        flex-direction: column;
        gap: 16px;
        margin-top: 20px;
        padding: 20px;
        background: var(--primary-bg);
        border-radius: var(--border-radius);
        box-shadow: var(--shadow-large);
        border: 2px solid var(--accent-color);
      }
      .download-items {
        display: flex;
        gap: 12px;
        flex-wrap: wrap;
      }
      .download-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 12px;
        background: var(--primary-bg);
        border-radius: var(--border-radius);
        box-shadow: var(--shadow-small);
        border: 1px solid rgba(102, 126, 234, 0.1);
        flex: 1;
        min-width: 280px;
      }
      .download-title {
        font-size: 0.95rem;
        font-weight: 600;
        color: var(--text-primary);
        display: flex;
        align-items: center;
        gap: 6px;
      }
      .download-size {
        font-size: 0.85rem;
        color: var(--text-secondary);
        font-weight: 500;
        padding: 4px 10px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        border: 1px solid rgba(102, 126, 234, 0.2);
      }

      footer {
        margin-top: 30px;
        text-align: center;
        color: var(--text-secondary);
        font-size: 0.95rem;
        padding: 20px;
        background: var(--primary-bg);
        border-radius: 20px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
      }
    </style>
    <script src="./text-to-image/html-to-image.min.js"></script>
  </head>
  <body>
    <div class="container">
      <header>
        <h1>文字转图片</h1>
        <p class="subtitle">从剪贴板粘贴并保留原有格式，一键导出高清图片</p>
      </header>

      <div class="panels">
        <section class="panel">
          <div class="panel-title" data-icon="📋">粘贴内容</div>
          <div class="file-upload paste-wrapper">
            <div
              id="pasteArea"
              contenteditable="true"
              aria-label="粘贴区域"
              role="textbox"
            ></div>
            <div id="pastePlaceholder">
              在此按 Ctrl+V / ⌘+V
              粘贴内容，支持富文本（加粗、颜色、列表、图片等）
            </div>
          </div>
          <div class="actions">
            <button class="btn-primary" id="btnFocusPaste">获取粘贴板</button>
            <button id="btnClear" class="btn-danger">清空内容</button>
          </div>
        </section>

        <section class="panel">
          <div class="panel-title" data-icon="⚙️">导出选项</div>
          <form class="options-form" id="optionsForm">
            <div class="form-group">
              <label for="outputFormat">输出格式</label>
              <select id="outputFormat">
                <option value="png">PNG（透明背景可选）</option>
                <option value="jpeg">JPEG</option>
              </select>
            </div>
            <div class="form-group">
              <label for="canvasWidth">画布宽度（px）</label>
              <input
                type="number"
                id="canvasWidth"
                min="320"
                max="4096"
                value="1024"
              />
            </div>
            <div class="form-group">
              <label for="canvasPadding">内边距（px）</label>
              <input
                type="number"
                id="canvasPadding"
                min="0"
                max="200"
                value="24"
              />
            </div>
            <div class="form-group">
              <label for="scaleFactor">缩放倍数（像素密度）</label>
              <input type="number" id="scaleFactor" min="1" max="4" value="2" />
            </div>
            <div class="form-group">
              <label for="bgTransparent">透明背景（仅PNG）</label>
              <select id="bgTransparent">
                <option value="true">开启</option>
                <option value="false" selected>关闭</option>
              </select>
            </div>
            <div class="form-group">
              <label for="bgColor">背景颜色（关闭透明时生效）</label>
              <input type="color" id="bgColor" value="#ffffff" />
            </div>
            <div class="form-group">
              <label for="bgPreset">背景图预设</label>
              <select id="bgPreset">
                <option value="none" selected>无背景图</option>
                <option value="gradient-soft">柔和渐变</option>
                <option value="grid">浅网格</option>
                <option value="dots">浅圆点</option>
                <option value="lines">横线纸纹</option>
                <option value="dark-gradient">深色渐变</option>
              </select>
            </div>
            <div class="form-group">
              <label for="removeTextBg">移除文本背景色</label>
              <select id="removeTextBg">
                <option value="off" selected>关闭</option>
                <option value="on">开启（清理粘贴内容中背景色）</option>
              </select>
            </div>
            <div class="form-group">
              <label for="bgImageFile">本地背景图</label>
              <div>
                <input type="file" id="bgImageFile" accept="image/*" />
                <button
                  type="button"
                  id="btnRemoveBgImage"
                  class="btn-danger"
                  style="margin-left: 8px"
                >
                  移除背景图
                </button>
                <div
                  id="bgImageInfo"
                  style="
                    margin-top: 6px;
                    font-size: 0.9rem;
                    color: var(--text-secondary);
                  "
                >
                  未选择图片
                </div>
              </div>
            </div>
          </form>

          <div class="progress-container">
            <div class="progress-bar">
              <div class="progress-fill" id="progressFill"></div>
            </div>
            <div class="progress-text" id="progressText">准备就绪</div>
          </div>

          <div class="actions">
            <button class="btn-success" id="btnPreview">生成预览</button>
            <button class="btn-primary" id="btnDownload">导出并下载</button>
          </div>
        </section>
      </div>

      <section class="panel">
        <div class="panel-title" data-icon="🖼️">预览与下载</div>
        <div
          class="capture-outer"
          style="
            position: absolute;
            left: -10000px;
            top: -10000px;
            opacity: 0;
            pointer-events: none;
          "
        >
          <div id="captureArea"></div>
        </div>
        <div class="status-message" id="statusMsg"></div>
        <div class="error-message" id="errorMsg"></div>

        <div
          class="download-section"
          id="downloadSection"
          style="display: none"
        >
          <div class="download-items">
            <div class="download-item">
              <div class="download-title">图片预览</div>
              <div>
                <span class="download-size" id="imageSize">-</span>
                <a id="downloadLink" class="tool-link" download>下载图片</a>
              </div>
            </div>
          </div>
          <img
            id="previewImg"
            alt="导出预览"
            style="max-width: 100%; border-radius: 12px"
          />
        </div>
      </section>

      <footer>
        本工具完全在浏览器中执行，不上传任何数据。若粘贴后未保留样式，请直接在粘贴区域按
        Ctrl+V。
      </footer>
    </div>

    <script>
      const pasteArea = document.getElementById('pasteArea')
      const pastePlaceholder = document.getElementById('pastePlaceholder')
      const captureArea = document.getElementById('captureArea')
      const btnFocusPaste = document.getElementById('btnFocusPaste')
      const btnClear = document.getElementById('btnClear')
      const btnPreview = document.getElementById('btnPreview')
      const btnDownload = document.getElementById('btnDownload')

      const outputFormat = document.getElementById('outputFormat')
      const canvasWidth = document.getElementById('canvasWidth')
      const canvasPadding = document.getElementById('canvasPadding')
      const scaleFactor = document.getElementById('scaleFactor')
      const bgTransparent = document.getElementById('bgTransparent')
      const bgColor = document.getElementById('bgColor')
      const bgPreset = document.getElementById('bgPreset')
      const removeTextBg = document.getElementById('removeTextBg')
      const bgImageFile = document.getElementById('bgImageFile')
      const btnRemoveBgImage = document.getElementById('btnRemoveBgImage')
      const bgImageInfo = document.getElementById('bgImageInfo')
      let bgImageUrl = null

      const progressFill = document.getElementById('progressFill')
      const progressText = document.getElementById('progressText')
      const statusMsg = document.getElementById('statusMsg')
      const errorMsg = document.getElementById('errorMsg')
      const downloadSection = document.getElementById('downloadSection')
      const downloadLink = document.getElementById('downloadLink')
      const previewImg = document.getElementById('previewImg')
      const imageSize = document.getElementById('imageSize')

      function setProgress(p, text) {
        progressFill.style.width = Math.max(0, Math.min(100, p)) + '%'
        progressText.textContent = text || ''
      }

      function applyCanvasLayout() {
        const width = parseInt(canvasWidth.value, 10) || 1024
        const padding = parseInt(canvasPadding.value, 10) || 24
        captureArea.style.width = width + 'px'
        captureArea.style.padding = padding + 'px'
        const transparent =
          bgTransparent.value === 'true' && outputFormat.value === 'png'

        // 清理背景样式，避免预设之间残留
        captureArea.style.backgroundImage = 'none'
        captureArea.style.backgroundSize = ''
        captureArea.style.backgroundPosition = ''
        captureArea.style.backgroundColor = ''
        captureArea.style.background = ''

        if (transparent) {
          captureArea.style.background = 'transparent'
          return
        }

        // 本地背景图优先
        if (bgImageUrl) {
          captureArea.style.backgroundColor = bgColor.value
          captureArea.style.backgroundImage = `url(${bgImageUrl})`
          captureArea.style.backgroundSize = 'cover'
          captureArea.style.backgroundPosition = 'center'
          captureArea.style.backgroundRepeat = 'no-repeat'
          return
        }

        if (bgPreset && bgPreset.value && bgPreset.value !== 'none') {
          applyPresetBackground(bgPreset.value)
          return
        }

        captureArea.style.background = bgColor.value
      }

      function applyPresetBackground(key) {
        // 统一清空以避免残留
        captureArea.style.backgroundImage = 'none'
        captureArea.style.backgroundSize = ''
        captureArea.style.backgroundPosition = ''
        captureArea.style.backgroundColor = ''
        captureArea.style.background = ''

        switch (key) {
          case 'gradient-soft':
            captureArea.style.background =
              'linear-gradient(135deg, #f8fbff 0%, #eaf3ff 50%, #fdfbff 100%)'
            break
          case 'grid':
            captureArea.style.backgroundColor = bgColor.value
            captureArea.style.backgroundImage =
              'linear-gradient(#e6eaf3 1px, transparent 1px), linear-gradient(90deg, #e6eaf3 1px, transparent 1px)'
            captureArea.style.backgroundSize = '24px 24px, 24px 24px'
            captureArea.style.backgroundPosition = '0 0, 0 0'
            break
          case 'dots':
            captureArea.style.backgroundColor = bgColor.value
            captureArea.style.backgroundImage =
              'radial-gradient(#dfe5ef 0.9px, transparent 0.9px)'
            captureArea.style.backgroundSize = '18px 18px'
            captureArea.style.backgroundPosition = '0 0'
            break
          case 'lines':
            captureArea.style.backgroundColor = bgColor.value
            captureArea.style.backgroundImage =
              'linear-gradient(#e9effa 2px, transparent 2px)'
            captureArea.style.backgroundSize = '100% 32px'
            captureArea.style.backgroundPosition = '0 0'
            break
          case 'dark-gradient':
            captureArea.style.background =
              'linear-gradient(135deg, #232526 0%, #414345 100%)'
            break
          default:
            captureArea.style.background = bgColor.value
        }
      }

      function showPlaceholderIfEmpty() {
        const isEmpty = pasteArea.innerHTML.trim() === ''
        pastePlaceholder.style.display = isEmpty ? 'block' : 'none'
      }

      function mirrorToCapture() {
        // 将粘贴内容直接映射到捕获区域，保留原HTML结构
        captureArea.innerHTML = pasteArea.innerHTML || ''
        applyCanvasLayout()
      }

      // 处理粘贴事件，优先使用text/html以保留格式
      pasteArea.addEventListener('paste', (e) => {
        try {
          e.preventDefault()
          const html = e.clipboardData.getData('text/html')
          const text = e.clipboardData.getData('text/plain')
          if (html) {
            pasteArea.innerHTML = html
          } else {
            // 纯文本时保留换行
            pasteArea.textContent = text
          }
          if (removeTextBg && removeTextBg.value === 'on') {
            stripBackgroundStyles(pasteArea)
          }
          showPlaceholderIfEmpty()
          mirrorToCapture()
        } catch (err) {
          console.warn('处理粘贴内容失败，回退到默认行为', err)
        }
      })

      // 输入变更时同步
      pasteArea.addEventListener('input', () => {
        showPlaceholderIfEmpty()
        mirrorToCapture()
      })

      btnFocusPaste.addEventListener('click', async () => {
        // 读取剪贴板内容，优先获取 HTML 保留格式
        try {
          if (navigator.clipboard && navigator.clipboard.read) {
            const items = await navigator.clipboard.read()
            let html = ''
            let text = ''
            for (const item of items) {
              if (item.types.includes('text/html')) {
                const blob = await item.getType('text/html')
                html = await blob.text()
                break
              }
              if (item.types.includes('text/plain')) {
                const blob = await item.getType('text/plain')
                text = await blob.text()
              }
            }
            if (html) {
              pasteArea.innerHTML = html
            } else if (text) {
              pasteArea.textContent = text
            } else {
              statusMsg.textContent = '剪贴板中无可读取的文本内容'
            }
          } else if (navigator.clipboard && navigator.clipboard.readText) {
            const text = await navigator.clipboard.readText()
            pasteArea.textContent = text
          } else {
            statusMsg.textContent =
              '当前浏览器不支持直接读取剪贴板，请使用 Ctrl+V'
            return
          }
          if (removeTextBg && removeTextBg.value === 'on') {
            stripBackgroundStyles(pasteArea)
          }
          showPlaceholderIfEmpty()
          mirrorToCapture()
          statusMsg.textContent = '已从剪贴板读取内容'
        } catch (err) {
          console.error(err)
          errorMsg.textContent =
            '读取剪贴板失败：' + (err && err.message ? err.message : '未知错误')
        }
      })
      btnClear.addEventListener('click', () => {
        pasteArea.innerHTML = ''
        showPlaceholderIfEmpty()
        mirrorToCapture()
        statusMsg.textContent = ''
        errorMsg.textContent = ''
        downloadSection.style.display = 'none'
        setProgress(0, '已清空')
      })

      // 背景图上传与移除
      if (bgImageFile) {
        bgImageFile.addEventListener('change', () => {
          try {
            const file = bgImageFile.files && bgImageFile.files[0]
            if (!file) return
            if (bgImageUrl) {
              URL.revokeObjectURL(bgImageUrl)
              bgImageUrl = null
            }
            bgImageUrl = URL.createObjectURL(file)
            if (bgImageInfo)
              bgImageInfo.textContent = `${file.name} (${Math.round(
                file.size / 1024
              )} KB)`
            applyCanvasLayout()
          } catch (err) {
            console.warn('加载背景图失败', err)
          }
        })
      }
      if (btnRemoveBgImage) {
        btnRemoveBgImage.addEventListener('click', () => {
          try {
            if (bgImageUrl) {
              URL.revokeObjectURL(bgImageUrl)
              bgImageUrl = null
            }
            if (bgImageFile) bgImageFile.value = ''
            if (bgImageInfo) bgImageInfo.textContent = '未选择图片'
            applyCanvasLayout()
          } catch (err) {
            console.warn('移除背景图失败', err)
          }
        })
      }

      // 选项更新时应用布局
      ;[
        outputFormat,
        canvasWidth,
        canvasPadding,
        scaleFactor,
        bgTransparent,
        bgColor,
        bgPreset,
        removeTextBg,
      ].forEach((el) => {
        el.addEventListener('change', () => {
          if (el === removeTextBg && removeTextBg.value === 'on') {
            stripBackgroundStyles(pasteArea)
            mirrorToCapture()
          }
          applyCanvasLayout()
        })
      })

      function stripBackgroundStyles(root) {
        try {
          const nodes = root.querySelectorAll('*')
          nodes.forEach((el) => {
            if (el.hasAttribute('bgcolor')) {
              el.removeAttribute('bgcolor')
            }
            const style = el.getAttribute('style') || ''
            if (style) {
              const cleaned = style
                .replace(/background[^;]*;?/gi, '')
                .replace(/\s{2,}/g, ' ')
              if (cleaned.trim().length === 0) {
                el.removeAttribute('style')
              } else {
                el.setAttribute('style', cleaned)
              }
            }
            el.style.background = ''
            el.style.backgroundColor = ''
          })
        } catch (err) {
          console.warn('移除背景样式失败', err)
        }
      }

      async function renderImage(previewOnly = true) {
        errorMsg.textContent = ''
        statusMsg.textContent = '正在生成图片…'
        setProgress(15, '布局中')
        applyCanvasLayout()

        await new Promise((r) => setTimeout(r, 100))
        setProgress(45, '渲染中')

        const scale = Math.max(
          1,
          Math.min(4, parseInt(scaleFactor.value, 10) || 2)
        )
        const isTransparent =
          bgTransparent.value === 'true' && outputFormat.value === 'png'
        const backgroundColor =
          isTransparent || bgImageUrl || (bgPreset && bgPreset.value !== 'none')
            ? undefined
            : bgColor.value

        try {
          const node = captureArea
          const toImage =
            outputFormat.value === 'png'
              ? htmlToImage.toPng
              : htmlToImage.toJpeg
          const dataUrl = await toImage(node, {
            pixelRatio: scale,
            backgroundColor,
            cacheBust: true,
            // 指定尺寸以避免滚动截断
            width: node.scrollWidth,
            height: node.scrollHeight,
            style: {
              transform: 'none',
            },
          })

          setProgress(85, '完成')

          const bytes = Math.round((dataUrl.length * 0.75) / 1024) // 粗略估算
          imageSize.textContent = bytes + ' KB'
          previewImg.src = dataUrl
          downloadLink.href = dataUrl
          const ext = outputFormat.value === 'png' ? 'png' : 'jpg'
          downloadLink.download = `text-image.${ext}`
          downloadSection.style.display = 'block'
          statusMsg.textContent = previewOnly
            ? '预览已生成'
            : '图片已生成，点击下载'
          setProgress(100, '完成')
        } catch (err) {
          console.error(err)
          errorMsg.textContent =
            '生成图片失败：' + (err && err.message ? err.message : '未知错误')
          statusMsg.textContent = ''
          setProgress(0, '失败')
        }
      }

      btnPreview.addEventListener('click', () => renderImage(true))
      btnDownload.addEventListener('click', () => renderImage(false))

      // 初始状态
      showPlaceholderIfEmpty()
      mirrorToCapture()
      setProgress(0, '准备就绪')
    </script>
  </body>
</html>
